﻿#region Using Directives

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Web;
using System.Web.UI.WebControls.WebParts;
using System.Xml;
using System.Xml.Serialization;

using Microsoft.SharePoint;
using Microsoft.SharePoint.WebPartPages;

using StooRob.SharePoint.ContentUpdater.Utilities;

#endregion Using Directives

namespace StooRob.SharePoint.ContentUpdater
{
    /// <summary>
    /// SharePoint content updater utility class.  Provides mechanism to add, update and remove Web Parts from SharePoint pages.
    /// </summary>
    public sealed class ContentUpdater
    {
        #region Private Members

        /// <summary>
        /// Private singleton member
        /// </summary>
        static volatile ContentUpdater instance;

        /// <summary>
        /// Private locking object.
        /// </summary>
        static object syncRoot = new object();

        readonly string CLASS_TITLE;

        #endregion Private Members

        #region Constructor

        /// <summary>
        /// Private constructor for singleton class.
        /// </summary>
        private ContentUpdater()
        {
            CLASS_TITLE = this.GetType().Name;
        }

        #endregion Constructor

        #region Public Properties

        /// <summary>
        /// Gets the singleton <see cref="T:StooRob.SharePoint.ContentUpdater.ContentUpdater"/> Instance object.
        /// </summary>
        public static ContentUpdater Instance
        {
            get
            {
                if (instance == null)
                {
                    lock (syncRoot)
                    {
                        if (instance == null)
                        {
                            instance = new ContentUpdater();
                        }
                    }
                }
                return instance;
            }
        }

        #endregion Public Properties

        #region Public Methods

        /// <summary>
        /// Perform all add, update and remove actions defined in the update actions XML provided.
        /// </summary>
        /// <param name="actionsXML">String containing content update XML to process.</param>
        public void ProcessActions(string actionsXML)
        {
            if (string.IsNullOrEmpty(actionsXML))
            {
                Trace.WriteLine("The Content Update XML was not provided.", CLASS_TITLE);
            }
            else
            {
                try
                {
                    ContentUpdateActions updateActions = GetContentUpdateActionsObject(actionsXML);
                    if (updateActions != null)
                    {
                        ProcessActions(updateActions);
                    }
                }
                catch (Exception ex)
                {
                    Trace.WriteLine(string.Format("{0} exception in ProcessActions for Action: {1}", this.GetType().Namespace, ex.Message), CLASS_TITLE);
                    throw ex;
                }
            }
        }

        /// <summary>
        /// Perform all add, update and remove actions defined in the <see cref="T:StooRob.SharePoint.ContentUpdater.ContentUpdateActions"/> object provided.
        /// </summary>
        /// <param name="actions">The <see cref="T:StooRob.SharePoint.ContentUpdater.ContentUpdateActions"/> object to process.</param>
        public void ProcessActions(ContentUpdateActions actions)
        {
            if (actions != null && actions.Actions != null && actions.Actions.Count > 0)
            {
                Debug.WriteLine("Number of Actions: " + actions.Actions.Count.ToString(), CLASS_TITLE);
                foreach (Action action in actions.Actions)
                {
                    try
                    {
                        ProcessAction(action);
                    }
                    catch (Exception ex)
                    {
                        Trace.WriteLine(string.Format("{0} exception in ProcessActions for Action {1}: {2}", this.GetType().Namespace, GetActionURL(action), ex.Message), CLASS_TITLE);
                        throw ex;
                    }
                }
            }
            else
            {
                Trace.WriteLine("The Content Update object did not contain any actions.", CLASS_TITLE);
            }
        }

        #endregion Public Methods

        #region Private Methods

        /// <summary>
        /// Process an individual <see cref="T:StooRob.SharePoint.ContentUpdater.Action"/> object.
        /// </summary>
        /// <param name="action">The <see cref="T:StooRob.SharePoint.ContentUpdater.Action"/> object to process.</param>
        private void ProcessAction(Action action)
        {
            if (action != null && action.ContentUpdates != null && action.ContentUpdates.Count > 0)
            {
                if (string.IsNullOrEmpty(action.Url))
                {
                    Trace.WriteLine("The Url attribute was not provided.", CLASS_TITLE);
                    return;
                }
                if (string.IsNullOrEmpty(action.Page))
                {
                    Trace.WriteLine("The Page attribute was not provided.", CLASS_TITLE);
                    return;
                }

                Debug.WriteLine(string.Format("Number of Content Updates: {0} for [{1}].", action.ContentUpdates.Count.ToString(), action.Page), CLASS_TITLE);

                using (SPSite site = new SPSite(action.Url))
                {
                    using (SPWeb web = site.OpenWeb(action.Site == null ? string.Empty : action.Site))
                    {
                        SPFile page = web.GetFile(string.Concat(web.Url.ToString(), action.Page));
                        if (page != null)
                        {
                            bool pageUpdated = false;
                            bool performCheckIn = true;
                            try
                            {
                                if (page.CheckOutStatus == SPFile.SPCheckOutStatus.None)
                                {
                                    // The page must be checked out prior to retrieveing the WebPart Manager, otherwise changes may fail.
                                    page.CheckOut();
                                }
                                else if (page.CheckedOutBy.ID == web.CurrentUser.ID)
                                {
                                    // Don't check the page in as it wasn't checked out by this process.
                                    performCheckIn = false;
                                }
                                else
                                {
                                    throw new AlreadyCheckedOutException(page.Url, page.CheckedOutBy, page.CheckOutStatus);
                                }

                                using (SPLimitedWebPartManager limitedWebPartManager = page.GetLimitedWebPartManager(PersonalizationScope.Shared))
                                {
                                    // Iterate through the collection of Web Parts on the current page.
                                    for (int ii = 0; ii < limitedWebPartManager.WebParts.Count; ii++)
                                    {
                                        System.Web.UI.WebControls.WebParts.WebPart webPart = limitedWebPartManager.WebParts[ii];

                                    // Iterate through the collection of Web Parts on the current page.
                                    //foreach (System.Web.UI.WebControls.WebParts.WebPart webPart in limitedWebPartManager.WebParts)
                                    //{
                                        Type wpType = webPart.GetType();

                                        if (wpType != null)
                                        {
                                            // Check if there is a Web Part in the update list matching the current Web Part.
                                            ContentUpdate contentToUpdate = action.ContentUpdates.Find(delegate(ContentUpdate update)
                                                {
                                                    // Match by namespace, zone title and web part title.
                                                    return update.WebPartNamespace == wpType.FullName &&
                                                           string.Compare(update.WebPartZoneTitle, limitedWebPartManager.GetZoneID(webPart), StringComparison.InvariantCultureIgnoreCase) == 0 &&
                                                           string.Compare(update.WebPartTitle, webPart.DisplayTitle, StringComparison.InvariantCultureIgnoreCase) == 0;
                                                });

                                            if (contentToUpdate != null)
                                            {
                                                switch (contentToUpdate.UpdateType)
                                                {
                                                    case UpdateTypes.Update:
                                                        pageUpdated |= PerformUpdate(contentToUpdate, limitedWebPartManager, webPart);
                                                        break;
                                                    case UpdateTypes.Remove:
                                                        pageUpdated |= PerformRemove(limitedWebPartManager, webPart);
                                                        break;
                                                }
                                            }
                                        }
                                    }

                                    foreach (ContentUpdate addContent in action.ContentUpdates)
                                    {
                                        if (addContent.UpdateType == UpdateTypes.Add)
                                        {
                                            pageUpdated |= PerformAdd(limitedWebPartManager, addContent);
                                        }
                                    }
                                }
                            }
                            catch (Exception ex)
                            {
                                throw ex;
                            }
                            finally
                            {
                                if (performCheckIn)
                                {
                                    if (pageUpdated)
                                    {
                                        page.CheckIn("System Content Update");
                                    }
                                    else
                                    {
                                        page.UndoCheckOut();
                                    }
                                }

                                if (pageUpdated)
                                {
                                    page.Update();
                                }
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Process all updates for the current content in the action list.
        /// </summary>
        /// <param name="contentToUpdate">The content definition for updating</param>
        /// <param name="limitedWebPartManager">The <see cref="Microsoft.SharePoint.WebPartPages.SPLimitedWebPartManager"/>object for the page requiring updating.</param>
        /// <param name="webPart">The <see cref="T:System.Web.UI.WebControls.WebParts.WebPart"/> object to update.</param>
        private bool PerformUpdate(ContentUpdate contentToUpdate, SPLimitedWebPartManager limitedWebPartManager, System.Web.UI.WebControls.WebParts.WebPart webPart)
        {
            bool result = false;
            if (contentToUpdate.PropertyUpdates != null && contentToUpdate.PropertyUpdates.Count > 0)
            {
                Type wpType = webPart.GetType();

                foreach (PropertyUpdate property in contentToUpdate.PropertyUpdates)
                {
                    if (string.IsNullOrEmpty(property.Name))
                    {
                        Trace.WriteLine(string.Format("The property name attribute was not provided for {0}.", contentToUpdate.WebPartNamespace), CLASS_TITLE);
                        break;
                    }
                    if (string.IsNullOrEmpty(property.Value))
                    {
                        Trace.WriteLine(string.Format("The property value attribute was not provided for {0}.", contentToUpdate.WebPartNamespace), CLASS_TITLE);
                        break;
                    }

                    // Get the type property to be updated from the current Web Part in the limitedWebPartManager collection
                    PropertyInfo propertyInfo = wpType.GetProperty(property.Name);
                    if (propertyInfo != null)
                    {
                        // Get the type of the property to set and ensure the new value can be converted to it.
                        TypeConverter conv = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
                        if (conv.CanConvertFrom(typeof(string)))
                        {
                            object value = conv.ConvertFrom(property.Value);
                            propertyInfo.SetValue(webPart, value, null);

                            // Update the Web Part's property with the new value.
                            bool allowUnsafeUpdates = limitedWebPartManager.Web.AllowUnsafeUpdates;
                            try
                            {
                                limitedWebPartManager.Web.AllowUnsafeUpdates = true;
                                limitedWebPartManager.SaveChanges(webPart);
                                result = true;
                            }
                            catch (Exception ex)
                            {
                                Trace.WriteLine(string.Format("{0} exception in PerformUpdate: {1}", this.GetType().Namespace, ex.Message));
                                throw ex;
                            }
                            finally
                            {
                                limitedWebPartManager.Web.AllowUnsafeUpdates = allowUnsafeUpdates;
                            }
                        }
                    }
                }
            }

            return result;
        }

        /// <summary>
        /// Process the deletion of the WebPart for the current content in the action list.
        /// </summary>
        /// <param name="limitedWebPartManager">The <see cref="Microsoft.SharePoint.WebPartPages.SPLimitedWebPartManager"/>object for the WebPart requiring deletion.</param>
        /// <param name="webPart">The <see cref="T:System.Web.UI.WebControls.WebParts.WebPart"/> object to delete.</param>
        private bool PerformRemove(SPLimitedWebPartManager limitedWebPartManager, System.Web.UI.WebControls.WebParts.WebPart webPart)
        {
            bool result = false;
            bool allowUnsafeUpdates = limitedWebPartManager.Web.AllowUnsafeUpdates;
            try
            {
                limitedWebPartManager.Web.AllowUnsafeUpdates = true;
                limitedWebPartManager.DeleteWebPart(webPart);
                result = true;
            }
            catch (Exception ex)
            {
                Trace.WriteLine(string.Format("{0} exception in PerformRemove: {1}", this.GetType().Namespace, ex.Message));
                throw ex;
            }
            finally
            {
                limitedWebPartManager.Web.AllowUnsafeUpdates = allowUnsafeUpdates;
            }

            return result;
        }

        /// <summary>
        /// Process the addition of the WebPart for the current content in the action list.
        /// </summary>
        /// <param name="limitedWebPartManager">The <see cref="Microsoft.SharePoint.WebPartPages.SPLimitedWebPartManager"/> object for the WebPart to add.</param>
        /// <param name="update">The content definition containing the information to add content to the page.</param>
        private bool PerformAdd(SPLimitedWebPartManager limitedWebPartManager, ContentUpdate update)
        {
            bool result = false;

            if (!string.IsNullOrEmpty(update.Value))
            {
                bool allowUnsafeUpdates = limitedWebPartManager.Web.AllowUnsafeUpdates;
                try
                {
                    StringReader srDefinition = new StringReader(update.Value);
                    XmlReader reader = new XmlTextReader(srDefinition);
                    string errorMsg;

                    limitedWebPartManager.Web.AllowUnsafeUpdates = true;
                    System.Web.UI.WebControls.WebParts.WebPart webPart = limitedWebPartManager.ImportWebPart(reader, out errorMsg);

                    if (webPart != null)
                    {
                        limitedWebPartManager.AddWebPart(webPart, update.WebPartZoneTitle, update.WebPartZoneIndex > 0 ? update.WebPartZoneIndex : 1);
                        result = true;
                    }
                    else
                    {
                        throw new ImportWebPartExcepton(update.WebPartNamespace, update.WebPartZoneTitle, update.WebPartTitle, update.WebPartZoneIndex, errorMsg);
                    }
                }
                catch (ImportWebPartExcepton iex)
                {
                    throw iex;
                }
                catch (Exception ex)
                {
                    Trace.WriteLine(string.Format("{0} exception in PerformAdd: {1}", this.GetType().Namespace, ex.Message));
                    throw ex;
                }
                finally
                {
                    limitedWebPartManager.Web.AllowUnsafeUpdates = allowUnsafeUpdates;
                }
            }

            return result;
        }

        /// <summary>
        /// Get the full URL of the action object.
        /// </summary>
        /// <param name="action">The <see cref="T:StooRob.SharePoint.ContentUpdater.Action"/> object to get the URL for.</param>
        /// <returns>The URL for the action.</returns>
        private string GetActionURL(Action action)
        {
            return string.Concat(action.Url, action.Site, action.Page);
        }

        /// <summary>
        /// Deserializes the given XML string into a <see cref="T:StooRob.SharePoint.ContentUpdater.ContentUpdateActions"/> object.
        /// </summary>
        /// <param name="xml">XML string to deserialize into a <see cref="T:StooRob.SharePoint.ContentUpdater.ContentUpdateActions"/> object.</param>
        /// <returns>A <see cref="T:StooRob.SharePoint.ContentUpdater.ContentUpdateActions"/> object.</returns>
        private ContentUpdateActions GetContentUpdateActionsObject(string xml)
        {
            ContentUpdateActions actions = null;

            try
            {
                XmlSerializer serializer = new XmlSerializer(typeof(ContentUpdateActions));
                using (MemoryStream memoryStream = new MemoryStream(StringUtilities.StringToUTF8ByteArray(xml)))
                {
                    using (XmlTextWriter xmlTextWriter = new XmlTextWriter(memoryStream, Encoding.UTF8))
                    {
                        actions = serializer.Deserialize(memoryStream) as ContentUpdateActions;
                    }
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine(string.Format("{0} exception in GetContentUpdateActionsObject: {1}", this.GetType().Namespace, ex.Message), CLASS_TITLE);
                throw ex;
            }

            return actions;
        }

        #endregion Private Methods
    }
}
